﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using EntModApp.EntServices.Presentation;
using System.Collections.Specialized;
using System.Collections;

namespace EntModApp.EntServices
{
  [Serializable]
  public abstract class NotifyObject : 
    INotifyPropertyChanged, 
    INotifyPropertyChainChanged
  {
    private Dictionary<string, HashSet<string>> propertyDependencies;
    private HashSet<string> quickFiringProperties;
    private Dictionary<string, PropertyChangedEventArgs> firingProperties; // to avoid cyclic firing of properties
    private PropertyChangedEventArgs originalFiringProperty;
    private GenericDisposable firingMonitor;
    private bool fireChain;

    [field: NonSerialized]
    public event PropertyChangedEventHandler PropertyChanged;

    [field: NonSerialized]
    public event PropertyChainChangedEventHandler PropertyChainChanged;

    protected void OnPropertyChanged(PropertyChangedEventArgs args)
    {
      if (args == null) throw new ArgumentNullException("args");
      using (OpenFiringProperties())
      {
        EnqueueProperties(args);
        fireChain = true;
      }
    }

    protected void OnPropertyChainChanged(PropertyChainChangedEventArgs args)
    {
      if (args == null) throw new ArgumentNullException("args");
      using (OpenFiringProperties())
      {
        EnqueueProperties(null);
      }
    }

    protected virtual void OnPropertyChangedInternal(PropertyChangedEventArgs args)
    {
    }

    protected virtual void OnPropertyChainChangedInternal(PropertyChainChangedEventArgs args)
    {
    }

    protected virtual void TriggerPropertyChanged(PropertyChangedEventArgs args)
    {
      var handler = PropertyChanged;
      if (handler != null)
        handler(this, args);

      OnPropertyChangedInternal(args);
    }

    protected virtual void TriggerPropertyChainChanged(PropertyChainChangedEventArgs args)
    {
      var handler = PropertyChainChanged;
      if (handler != null)
        handler(this, args);

      OnPropertyChainChangedInternal(args);
    }

    private void EnqueueProperties(PropertyChangedEventArgs args)
    {
      lock (this)
      {
        var queue = new Queue<string>();
        if (args != null)
        {
          queue.Enqueue(args.PropertyName);
          firingProperties[args.PropertyName] = args;
          originalFiringProperty = originalFiringProperty ?? args;
        }
        if (quickFiringProperties != null)
          foreach (var prop in quickFiringProperties.Where(p => !firingProperties.ContainsKey(p)))
          {
            queue.Enqueue(prop);
            firingProperties[prop] = new PropertyChangedEventArgs(prop);
          }

        if (propertyDependencies != null)
          while (queue.Count > 0)
          {
            var propName = queue.Dequeue();
            //if (firingProperties.ContainsKey(propName))
            //  continue;

            HashSet<string> set;
            if (propertyDependencies.TryGetValue(propName, out set))
              foreach (var depProp in set)
              {
                if (firingProperties.ContainsKey(depProp))
                  continue;

                queue.Enqueue(depProp);
                firingProperties[depProp] = new PropertyChangedEventArgs(depProp);
              }
          }
      }
    }

    protected void OnPropertyChanged(string propertyName)
    {
      if (string.IsNullOrEmpty(propertyName))
        throw new ArgumentNullException("propertyName");
      OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    }

    protected void OnPropertyChanged<TValue>(Expression<Func<TValue>> propertyAccess)
    {
      if (propertyAccess == null)
        throw new ArgumentNullException("propertyAccess");
      var name = ((PropertyInfo)((MemberExpression)propertyAccess.Body).Member).Name;
      OnPropertyChanged(name);
    }

    protected void RegisterDependent(string propertyName, params string[] dependencies)
    {
      if (string.IsNullOrEmpty(propertyName))
        throw new ArgumentNullException("propertyName");
      if (dependencies == null)
        throw new ArgumentNullException("dependencies");
      if (dependencies.Any(d => string.IsNullOrEmpty(d)))
        throw new ArgumentNullException("dependencies");

      if (propertyDependencies == null)
        propertyDependencies = new Dictionary<string, HashSet<string>>();

      foreach (var dependency in dependencies)
      {
        HashSet<string> set;
        if (!propertyDependencies.TryGetValue(dependency, out set))
        {
          set = new HashSet<string>();
          propertyDependencies[dependency] = set;
        }
        set.Add(propertyName);
      }
    }
    protected void RegisterDependent<TValue, TValue1>(Expression<Func<TValue>> property, params Expression<Func<TValue1>>[] dependencies)
    {
      RegisterDependent(GetPropertyName(property), dependencies.Select(d => GetPropertyName(d)).ToArray());
    }
    protected void RegisterDependent<TValue, TValue1, TValue2>(Expression<Func<TValue>> property, Expression<Func<TValue1>> dependentProperty1, Expression<Func<TValue2>> dependentProperty2)
    {
      RegisterDependent(GetPropertyName(property), GetPropertyName(dependentProperty1), GetPropertyName(dependentProperty2));
    }
    protected void RegisterDependent<TValue, TValue1, TValue2, TValue3>(Expression<Func<TValue>> property, Expression<Func<TValue1>> dependentProperty1, Expression<Func<TValue2>> dependentProperty2, Expression<Func<TValue3>> dependentProperty3)
    {
      RegisterDependent(GetPropertyName(property), GetPropertyName(dependentProperty1), GetPropertyName(dependentProperty2), GetPropertyName(dependentProperty3));
    }
    protected void RegisterQuickFiring(string propertyName)
    {
      if (string.IsNullOrEmpty(propertyName))
        throw new ArgumentNullException("propertyName");

      if (quickFiringProperties == null)
        quickFiringProperties = new HashSet<string>();

      quickFiringProperties.Add(propertyName);
    }
    protected void RegisterQuickFiring<TValue>(Expression<Func<TValue>> property)
    {
      RegisterQuickFiring(GetPropertyName(property));
    }

    protected void RegisterChild(object child)
    {
      var inpcc = child as INotifyPropertyChainChanged;
      if (inpcc != null)
      {
        inpcc.PropertyChainChanged += Child_PropertyChainChanged;
      }
      else
      {
        var inpc = child as INotifyPropertyChanged;
        if (inpc != null)
        {
          inpc.PropertyChanged += new PropertyChangedEventHandler(Child_PropertyChanged);
        }

        var incc = child as INotifyCollectionChanged;
        if (incc != null)
        {
          incc.CollectionChanged += new NotifyCollectionChangedEventHandler(Child_CollectionChanged);

          var ie = child as IEnumerable;
          if (ie != null)
            foreach (var item in ie)
              RegisterChild(item);
        }
      }
    }
    protected void UnregisterChild(object child)
    {
      var inpcc = child as INotifyPropertyChainChanged;
      if (inpcc != null)
      {
        inpcc.PropertyChainChanged -= Child_PropertyChainChanged;
      }
      else
      {
        var inpc = child as INotifyPropertyChanged;
        if (inpc != null)
        {
          inpc.PropertyChanged -= new PropertyChangedEventHandler(Child_PropertyChanged);
        }

        var incc = child as INotifyCollectionChanged;
        if (incc != null)
        {
          incc.CollectionChanged -= new NotifyCollectionChangedEventHandler(Child_CollectionChanged);

          var ie = child as IEnumerable;
          if (ie != null)
            foreach (var item in ie)
              UnregisterChild(item);
        }
      }
    }

    private void Child_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
      foreach (var item in e.OldItems)
        UnregisterChild(item);
      foreach (var item in e.NewItems)
        RegisterChild(item);
    }
    private void Child_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
      var originalStep = new PropertyChainStep(sender, e);
      TriggerPropertyChainChanged(new PropertyChainChangedEventArgs(originalStep, new [] { originalStep }));
    }
    private void Child_PropertyChainChanged(object sender, PropertyChainChangedEventArgs e)
    {
      TriggerPropertyChainChanged(e);
    }

    private string GetPropertyName(LambdaExpression property)
    {
      return ((PropertyInfo)((MemberExpression)property.Body).Member).Name;
    }

    private IDisposable OpenFiringProperties()
    {
      if (firingMonitor == null)
      {
        lock (this)
        {
          if (firingMonitor == null)
          {
            firingMonitor = new GenericDisposable
            {
              EnterAction = () =>
                {
                  if (firingProperties == null)
                  {
                    firingProperties = new Dictionary<string, PropertyChangedEventArgs>();
                    fireChain = false;
                    originalFiringProperty = null;
                  }
                },
              ExitAction = disposing =>
                {
                  if (disposing)
                  {
                    HashSet<string> alreadyFired = new HashSet<string>();
                    PropertyChangedEventArgs[] cache = new PropertyChangedEventArgs[0];
                    while (firingProperties.Count > cache.Length)
                    {
                      cache = firingProperties.Values.ToArray();
                      foreach (var item in cache.Where(p => !alreadyFired.Contains(p.PropertyName)))
                      {
                        TriggerPropertyChanged(item);
                        fireChain = true;
                        alreadyFired.Add(item.PropertyName);
                      }
                    }
                    if (fireChain)
                    {
                      var originalStep = new PropertyChainStep(this, originalFiringProperty);
                      TriggerPropertyChainChanged(new PropertyChainChangedEventArgs(originalStep, cache.Select(c => new PropertyChainStep(this, c))));
                    }

                    firingProperties = null;
                    firingMonitor = null;
                    fireChain = false;
                    originalFiringProperty = null;
                  }
                },
            };
          }
        }
      }
      firingMonitor.Enter();
      return firingMonitor;
    }

    private class GenericDisposable : IDisposable
    {
      public Action EnterAction;
      public Action<bool> ExitAction;
      private int enterCount;

      public void Enter()
      {
        lock (this)
        {
          enterCount++;
          EnterAction();
        }
      }

      public void Dispose()
      {
        lock (this)
        {
          if (enterCount <= 0)
            throw new InvalidOperationException("Calling dispose before an enter operation");
          ExitAction(enterCount == 1);
          enterCount--;
        }
      }
    }

  }
}
